import {interval} from '@mootari/range-slider' // two ended slider
interval_format = ([start, end]) => `[${start}, ${end}]`
seasons = ["spring", "summer", "fall", "winter"]
filter_error = '<p class="filter-error"><i class="bi bi-exclamation-triangle"></i> No segments match selected filters</p>'
// all segments
s = transpose(segments)
// all segments' data
d = transpose(data).map(d => ({ ...d, time: new Date(d.time) }))Project Loon
Welcome to the datapage for Project Loon data! This dataset consists of 385 balloon flights, split into 938 data segments. For more details on the dataset, please see the About tab. On this page, we offer several tools to select and visualize data from individual balloons. The raw data can be viewed and downloaded from the Data tab, and the Analysis tab offers other data access and analysis tools. We’re currently working on more tools to visualize and access the data, so stay tuned!
ns = Inputs.text().classList[0]
// custom css to override some ojs defaults for inputs
html`<style>
.${ns} {
--label-width: 70px;
}
.${ns} div label {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
margin-right: 0.25rem;
width: auto;
}
.${ns} div label:hover,
.${ns} div label:active,
.${ns} div label:focus {
background-color: #cdecff;
}
.${ns} div input[type="number"] {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
flex-shrink: 3;
border: none;
}
.${ns} select {
background-color: #f4f4f4;
border: none;
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
width: auto;
}
}
</style>`Use these filters to narrow down the list of segments, and then select an individual segment from this list below to visualize its trajectory and telemetry.
// filter by season(s)
viewof season = Inputs.checkbox(seasons, {value: seasons, label: "Season (astronomical, northern hemisphere)"})
//viewof season = Inputs.checkbox(seasons, {value: ["winter"], label: "Season (astronomical)"})// filter by range of longitudes
viewof longitude_range = interval([-180, 180], {
step: 1, label: "Longitudes", width: "80%", format: interval_format
})sf = s.filter(d => season.some(se => d.season.includes(se)))
.filter(d => d.longitude_min <= longitude_range[1] &
d.longitude_max >= longitude_range[0])
.filter(d => d.latitude_min <= latitude_range[1] &
d.latitude_max >= latitude_range[0])
.filter(d => d.duration >= duration_range[0] &
d.duration <= duration_range[1])
// filtered segments' IDs
sf_id = sf.map(d => d.segment_id)
// filtered segments' data
df = d.filter(d => sf_id.includes(d.segment_id))land = FileAttachment("topo/ne_110m_land.json").json()
ocean = FileAttachment("topo/ne_110m_ocean.json").json()
land_color = "wheat"
ocean_color = "#8398aa"
// map -- globe
viewof map_globe = Plot.plot({
width: 400,
projection: {type: "orthographic", rotate: [-longitude, -latitude]},
marks: [
Plot.sphere(),
Plot.graticule(),
Plot.geo(ocean, {fill: ocean_color, opacity: 0.7}),
Plot.geo(land, {fill: land_color}),
Plot.line(df, {
x: "longitude", y: "latitude", z: "segment_id",
r: 0.7, stroke: "grey", strokeOpacity: 0.7,
}),
]
})d_found = sf.length > 0
html`${d_found ? '' : filter_error}`
dg = d3.group(df, d => d.segment_id)
viewof ds = Inputs.select(dg, {
value: dg.get(112),
format: ([k, v]) => `Segment ${k} (Flight ${v[0].flight_id})`
})
start_color = "var(--bs-success)"
end_color = "var(--bs-danger)"
print_date = d => d.toISOString().split('T')[0]
// map -- whole world equirectangular
d_found ? Plot.plot({
style: { fontFamily: "var(--sans-serif)" },
projection: { type: "equirectangular" },
marks: [
//Plot.sphere(),
Plot.frame(),
Plot.graticule(),
Plot.geo(ocean, {fill: ocean_color, opacity: 0.7}),
Plot.geo(land, {fill: land_color}),
//Plot.geo(land, {fill: "lightgrey"}),
Plot.line(ds, {
x: "longitude", y: "latitude",
r: 1, stroke: "black", strokeOpacity: 0.3,
}),
Plot.dot(ds.slice(0, 1), {
x: "longitude", y: "latitude",
r: 3, fill: start_color, symbol: "triangle"
}),
Plot.text(ds.slice(0, 1), {
x: "longitude", y: "latitude",
text: d => print_date(d.time), dy: -7, lineAnchor: "bottom"
}),
Plot.dot(ds.slice(ds.length - 1, ds.length), {
x: "longitude", y: "latitude",
r: 3, fill: end_color, symbol: "square"
}),
Plot.text(ds.slice(ds.length - 1, ds.length), {
x: "longitude", y: "latitude",
text: d => print_date(d.time), dy: -7, lineAnchor: "bottom"
})
],
}) : html``
get_circle = (seg) => {
const minLon = d3.min(seg.map(d => d.longitude))
const maxLon = d3.max(seg.map(d => d.longitude))
const minLat = d3.min(seg.map(d => d.latitude))
const maxLat = d3.max(seg.map(d => d.latitude))
const center = [(minLon + maxLon) / 2, (minLat + maxLat) / 2]
const corners = [[minLon, minLat], [minLon, maxLat], [maxLon, minLat], [maxLon, maxLat]]
const dist = d3.max(corners, corner => d3.geoDistance(center, corner))
const radius = dist * (180 / Math.PI)
const circle = d3.geoCircle().center(center).radius(radius)()
return circle
}
circle = d_found ? get_circle(ds) : null
// map -- zoomed in azimuthal
d_found ? Plot.plot({
style: { fontFamily: "var(--sans-serif)" },
projection: { type: "azimuthal-equidistant", domain: circle },
marks: [
Plot.frame(),
Plot.graticule(),
Plot.geo(ocean, {fill: ocean_color, opacity: 0.7}),
Plot.geo(land, {fill: land_color}),
//Plot.geo(land, {fill: "lightgrey"}),
Plot.line(ds, {
x: "longitude", y: "latitude",
r: 1, stroke: "black", strokeOpacity: 0.3,
}),
Plot.dot(ds.slice(0, 1), {
x: "longitude", y: "latitude",
r: 6, fill: start_color, symbol: "triangle"
}),
Plot.text(ds.slice(0, 1), {
x: "longitude", y: "latitude",
text: d => print_date(d.time), dy: -7, lineAnchor: "bottom"
}),
Plot.dot(ds.slice(ds.length - 1, ds.length), {
x: "longitude", y: "latitude",
r: 6, fill: end_color, symbol: "square"
}),
Plot.text(ds.slice(ds.length - 1, ds.length), {
x: "longitude", y: "latitude",
text: d => print_date(d.time), dy: -7, lineAnchor: "bottom"
})
],
}) : html``
viewof scatter_altitude = Plot.plot({
style: { fontFamily: "var(--sans-serif)" },
width: 900,
height: 200,
inset: 8,
grid: true,
y: { label: "Altitude (meters)" },
marks: [
Plot.frame(),
Plot.dot(ds, { x: "time", y: "altitude", r: 0.5 }),
]
})
d_found ? html`
<form><label>Altitude</label></form>
${viewof scatter_altitude}
` : html``
winds = new Map([["Zonal (east-west)", "wind_u"],
["Meridional (north-south)", "wind_v"]])
viewof y_wind = Inputs.select(winds, {label: "Wind"})
viewof scatter_wind = Plot.plot({
style: { fontFamily: "var(--sans-serif)" },
width: 900,
height: 200,
inset: 8,
grid: true,
y: { label: "Velocity (meters/second)" },
marks: [
Plot.frame(),
Plot.dot(ds, { x: "time", y: y_wind, r: 0.5 }),
]
})
d_found ? html`
${viewof y_wind}
${viewof scatter_wind}
` : html``
fluxes = new Map([["Total", "flux_total"],
["Eastward", "flux_east"],
["Westward", "flux_west"],
["Northward", "flux_north"],
["Southward", "flux_south"]])
viewof y_flux = Inputs.select(fluxes, {label: "Flux"})
viewof scatter_flux = Plot.plot({
style: { fontFamily: "var(--sans-serif)" },
width: 900,
height: 200,
inset: 8,
grid: true,
y: { label: "Flux (millipascals)", transform: (f) => Math.abs(f * 1000) },
marks: [
Plot.frame(),
Plot.dot(ds, { x: "time", y: y_flux, r: 0.5 }),
]
})
d_found ? html`
${viewof y_flux}
${viewof scatter_flux}
` : html``